Skip to content

refactor(resources): one canonical view per resource, mounted on source/grants/host - #6279

Open
waleedlatif1 wants to merge 31 commits into
stagingfrom
improvement/resource-views-final
Open

refactor(resources): one canonical view per resource, mounted on source/grants/host#6279
waleedlatif1 wants to merge 31 commits into
stagingfrom
improvement/resource-views-final

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Every resource — file, table, log, knowledge — now has exactly one view, and every consumer mounts that one. The workspace page and the mothership panel render the same component; neither imports the other's implementation.

  • Three axes in apps/sim/resources/ (pure TS, no React) replace the ad-hoc props: source (workspace vs share, discriminated on via), grants (write/run/manage/settled), host (page/panel/public)
  • The panel no longer imports anything from @/app/workspace/[workspaceId]/{tables,knowledge,files,logs} — that coupling is what the axes exist to delete
  • scripts/check-resource-views.ts enforces it: no wrappers, no imports past a unit barrel, no route/permission context inside a unit, no workspaceId/canEdit/embedded props on a view. R6 has no annotation escape hatch by design
  • A share source structurally cannot carry a workspaceId, and a kind whose seed is never (table, knowledge, log) cannot be constructed anonymously at all — compile-time facts, not conventions

tables and knowledge were the two remaining shells. Both were severed from route context in place first (router → onNavigate, params → source, permission context → grants), so every semantic edit is reviewable as a diff against unmoved files and the move commits are import-only.

Intended behavior changes

Two, both fixing the panel writing to its host's URL:

  • The embedded table no longer writes sort/dir/table-view into /home's address bar. Both route pages keep their deep-linkable params unchanged
  • Opening a run from the tables page no longer writes ?tab=trace onto the tables URL

Everything else is byte-identical. grants.settled exists specifically so canEdit || isLoading gating survives the migration exactly rather than flickering on first paint.

Type of Change

  • Refactor

Testing

20951 tests passing. check:resources:strict, check:audits (23), type-check and lint green. Not yet browser-verified — the two URL-ownership changes and the header chrome on each surface are the pieces worth clicking through.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…ce/grants/host

Extracts the file and table views out of their route folders into canonical
units under `apps/sim/components/resources/**`, and gives every consumer one way
to mount them. Behavior-neutral: no page, no API route, and no migration is
added or removed, and every default is preserved.

The three axes (`apps/sim/resources/**`, pure TypeScript so a Server Component
can build one during SSR):

- `source` — where the data comes from and by what address, `WorkspaceSource<K> |
  ShareSource<K>` discriminated on `via`. Replaces `workspaceId`, `token`,
  `contentSource`, `isPublic`. `ShareSource` declares `workspaceId?: never`, so a
  share token can no longer be laundered through a workspace-shaped slot.
- `grants` — what this viewer may do: `{ write, run }`. Replaces `canEdit`,
  `canRun`, `canAdmin`, `disableEdit/Insert/Delete`.
- `host` — who owns the URL, the router, the document frame. Replaces
  `embedded`; `hostOwnsUrl(host)` is the single home of the "embedded views do
  not write nuqs keys" rule.

`file` is the only kind with a share seed, because `/f/[token]` is the only
public route that serves one. Every other kind is `never` and cannot be
addressed by a token at all — a compile-time fact rather than a runtime check.

Also in this PR:

- Public share auth (password / email-OTP / SSO) moves out of `app/f/[token]`
  into `components/public-share/**`, behind one page-side gate. Security
  hardening on the way: timing-safe OTP comparison, a per-IP bucket on OTP
  verify separate from the send path, and share tokens removed from logs.
- Log status chrome (`StatusBadge`, `STATUS_CONFIG`, `getDisplayStatus`) moves
  to `components/execution-status/`, shared instead of per-surface.
- `AnchoredContextMenu` and `ActionRow` move to `components/`, which closed a
  real hole: `file-view` is mounted by `app/f/[token]` for anonymous visitors,
  and with no `sideEffects: false` its import of the workspace barrel pulled the
  whole authenticated tree into the public chunk.
- emcn: `ChipPasswordInput`, a shared `ChipChevronDown`, and byte-range /
  untitled-title utilities behind the file preview routes.
- Deployed-chat header now renders the shared `Navbar` in `logoOnly` mode, so a
  public chat wears the same wordmark, geometry, name and "Shared by" credit as
  the shared file page. This drops the GitHub star chip from that header.
- `ActionRow` used `var(--divider)`, which is not a defined token, so its
  dividers fell back to `currentColor`. Now `var(--border)`.

Nothing here advertises a capability it cannot deliver: the `form` trigger type
that arrived with this work drives nothing on its own, so it is not registered in
the Start block's trigger list, the Logs filter, the execute contract, or the
executor's streaming types. It lands with the surface that produces it.

Enforced by `bun run check:resources` (added to CI), which ratchets counters for
wrappers, imports past a unit barrel, cross-tree imports, unsanctioned props,
token-as-workspaceId, and context leaks. The cross-tree counter is re-based to
the current tree: every offender it counts is inherited, and this change strictly
reduces the number.

Verification: apps/sim + 22 packages typecheck, biome clean, 18598 tests pass,
and all 15 repo gates pass including both strict variants.
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner August 5, 2026 03:37
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 8, 2026 5:25am

Request Review

@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large refactor of file viewing, public file egress, and share/auth paths touches security-sensitive anonymous surfaces and storage reads; URL ownership and rate-limit behavior need manual verification even with strict check:resources.

Overview
This PR locks in the resource-view contract (source / grants / host) with new .claude/.cursor rules and root CLAUDE.md / AGENTS.md updates, and requires check:resources alongside API validation on PRs.

Files are the focus of the migration in this diff: the workspace-local file-viewer tree is removed and every consumer mounts FileView from components/resources/file-view — including /f/[token], the fullscreen @-mention route, and (per the PR intent) panel vs page hosts with hostOwnsUrl so embedded views don’t write the parent’s nuqs keys. Shared list chrome (Resource, headers) is moved under @/components/resource so resource units don’t import the workspace route tree.

Public surfaces share one Navbar (logoOnly with resource name, “Shared by”, whitelabel, optional brandInNewTab) across auth, 404, shared files, and deployed chat; chat drops the bespoke GitHub header and adds ShareLinkButton plus sharedByName from the chat API. ShareModal is centralized in workspace components with upsertResourceShare; file provenance helpers move to lib/public-shares/provenance and the file page uses resolvePublicShareGate.

Serving and abuse controls expand materially: workspace and public file routes add HTTP byte-range streaming for audio/video (with MIME fixes and disposition rules), per-share content rate limits after auth, split per-IP buckets, OTP verify uses safeCompare, and chat/workflow execution uploads inline input.files on triggerType === 'chat'.

Reviewed by Cursor Bugbot for commit d512856. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile-apps

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR consolidates file and table resource rendering around canonical views parameterized by source, grants, and host, while reorganizing shared UI and public-share infrastructure.

  • Adds typed resource axes and canonical resource components for workspace, panel, and public surfaces.
  • Moves shared file, table, chat, public-share, context-menu, and presence components out of route-specific trees.
  • Refactors public file routes, table queries/services, and workspace consumers to use the shared architecture.
  • Adds a CI audit enforcing resource-view boundaries and removes obsolete trigger and icon-library usage.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up-review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/resources/source.ts Introduces discriminated workspace/share sources, preventing share sources from carrying workspace identifiers or producing workspace links.
apps/sim/components/resources/file-view/file-view.tsx Consolidates workspace, panel, streaming, and public file rendering into one canonical view driven by source, grants, and host.
apps/sim/app/f/[token]/public-file-view.tsx Mounts the canonical file view with a token-based share source, read-only grants, and public host behavior.
apps/sim/app/api/files/public/[token]/content/route.ts Refactors public content serving while retaining active-share resolution, deployment authentication, rate limiting, and ranged media responses before storage access.
apps/sim/components/resources/table-view/index.ts Establishes shared table-view exports and data contracts used by the workspace table grid.
apps/sim/hooks/queries/tables.ts Updates table query and mutation integration while preserving typed contracts and authoritative cache refresh behavior.
scripts/check-resource-views.ts Adds static enforcement for canonical mounts, import boundaries, axis purity, and prohibited route or permission context access.
.github/workflows/test-build.yml Adds the resource boundary audit to CI and removes the explicit ripgrep installation step.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Workspace[Workspace page] --> WS[Workspace source]
  Panel[Embedded panel] --> WS
  Public[Public share page] --> SS[Share source]
  Permissions[Workspace permissions] --> WG[Workspace grants]
  SharePolicy[Share policy] --> SG[Read-only share grants]
  WS --> View[Canonical resource view]
  SS --> View
  WG --> View
  SG --> View
  PageHost[page host: owns URL] --> View
  PanelHost[panel host: no URL ownership] --> View
  PublicHost[public host: no workspace links] --> View
  View --> File[File content and collaboration]
  View --> Table[Table rendering and editing]
Loading

Reviews (3): Last reviewed commit: "fix(file-view): correct the data-table s..." | Re-trigger Greptile

Navbar's actions slot is not gated by hideBrand, so a whitelabeled public
chat or shared file rendered a Share menu whose drafted post advertised
Sim. Compose the mention from brand.name when brand.isWhitelabeled,
matching the rule the email footer already applies.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile-apps

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c013365. Configure here.

data-table.tsx moved one directory deeper during the migration but kept
its './document-table.css' import. The stylesheet is shared at the
components/ level — both rich-markdown-editor importers already use
'../document-table.css' — so this was the lone stale path. TypeScript
does not resolve bare CSS imports, so only the production build caught it.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile-apps

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4da3df5. Configure here.

…e tab chrome (#6280)

Continues the resource-view layer. Three kinds now have a canonical view
(file, log, knowledge) and only `table` remains — deliberately, because the
mothership mounts its editing shell, which is a product decision rather than
unfinished work.

**log** — `log-details/` splits into `components/resources/log-view/` (the view)
and a 198-line shell that keeps resize, keyboard nav and close chrome. Four
leaked contexts resolved: `useParams` → `source.workspaceId`, `useRouter` →
an `onNavigate` prop, `useQueryState(tab)` → view-owned state gated by
`hostOwnsUrl(host)`, `usePermissionConfig` → a required `showExecutionInternals`
prop (deliberately not folded into `grants`: it gates *seeing* trace internals,
not write or run, and an optional field would default permissive — the wrong
failure mode for a permission-group restriction). A fifth leak the plan missed —
`file-download.tsx` holding its own `useRouter` — was caught by the gate.

**knowledge** — `base.tsx` (1733 lines) splits into a 909-line canonical view
and a 974-line shell. The read surface (document list, filters, sort,
pagination, unavailable state) is the view; upload, connectors, tag editing,
rename, delete and bulk operations stay in the shell, exactly as the tables
editing grid kept its write path. All six nuqs keys now route through
`useKnowledgeListState(host)`, whose every write sits behind `hostOwnsUrl`.

**tab chrome** — the five `Embedded*Actions` collapse into one kind-keyed
`ResourceTabActions`. They differed only in icon, copy and destination, which is
a config table. `knowledge` and `table` destinations now resolve through
`workspaceSource().hrefFor()` instead of hand-built strings.

**schedule** is removed as a resource kind: its page was deleted upstream, its
`resourceHref` case pointed at a route that no longer exists, and
`MothershipResourceType` never carried it, so it was unreachable.

Behavior change, intended and limited to embedded surfaces: the mothership panel
no longer writes the log `?tab` key or the six knowledge keys into the host
page's address bar. That is the bug the `host` axis exists to fix. Both route
pages keep their deep-linkable params unchanged.

`Resource` and `InlineRenameInput` moved to `components/` — mandatory, since a
canonical unit may not import the workspace route tree. The workspace barrel
re-exports both, so its consumers are byte-unchanged.

Ratchets: shadow-named components 8 → 2 (the 2 left are deliberate —
`EmbeddedWorkflow`, because a workflow is a live collaborative session rather
than a document with an address, and `EmbeddedFolder`, because a folder is
structure inside a resource); cross-tree imports 38 → 37. None raised.

Verification: apps/sim + 22 packages typecheck, biome clean, 18626 tests pass,
and all 15 repo gates pass including both strict variants. Not visually verified.
Conflict resolutions of substance:

- files serve + public-share content routes: kept the branch's byte-range
  media streaming and added staging's `ServeOptions` / `preview=1` HEIC
  derivative path. The two are disjoint (`isMediaContentType` is audio/video
  only), so both branches now coexist ahead of the buffered compile path.
- `hooks/use-file-content-source.tsx` is deleted on this branch; staging's
  additions to it (the `preview` URL flag and the `ImageDimensionsSource`
  capability) moved to `resources/file-source.ts`, and the workspace adapter
  now resolves from the mounted source rather than a second React context.
- `UnsupportedPreview`: the branch's download-carrying variant replaces the
  moved copy in `preview-shared`, so the image preview's new decode-failure
  fallback lands on the version that works on chrome-less hosts.
- logs: staging's workflow deep-link (#6275) ported into `LogView` through a
  new `resources/log-source.ts`, so the unit reads the href off its source
  instead of hand-building a workspace path.
- `public-file-view`: kept the branch's `Navbar` + `h-dvh` scroll port, which
  supersedes staging's header layout fix on the header it replaced.
- `test-build.yml`: took staging's single `check:audits` step; `run-audits.ts`
  derives the list from package.json, so `check:resources` is picked up.
- API route baseline raised to the merged tree's real count (1008).
- cloud-review-tools.test.ts: the merge kept the branch's import line (which
  had dropped `symlink` along with the test that used it) while taking
  staging's reinstated test body, so the symlink-escape assertion would have
  thrown ReferenceError on CI. tsconfig excludes *.test.ts, so type-check
  could not catch it. Restored the import and the workflow's ripgrep install
  step, which the branch removed alongside the same test.

- createByteRangeResponse defaulted to `public, max-age=31536000` while its
  sibling createFileResponse defaults to `private, no-cache` and says in a
  comment why. resolveServeCacheControl returns undefined for any unversioned
  non-`workspace` context, so those access-verified media responses landed on
  the public default — storable by a shared cache and re-servable cross-user,
  where staging sent `private, no-cache`. Defaults now match.

- ReadOnlyTextPreview: restored staging's `flex … flex-col` on the rich
  preview wrapper. Every PreviewPanel renderer sizes itself as a flex item, so
  in a block parent HtmlPreview measures a zero-height box, never mounts its
  iframe, and nothing ever grows the container to un-wedge it — shared .html
  and .svg rendered blank.
The branch's workflow listed only the non-strict variant; staging replaced the
hand-written step list with a runner that derives every zero-arg check:*. Merging
the two ran the 1359-line resource scanner twice. Excluded the same way, and for
the same reason, as check:api-validation.
…ated

A canonical unit is mounted by anonymous surfaces, so an import of the
authenticated route tree from inside one pulls the workspace bundle into the
public chunk and re-creates exactly the coupling the axes exist to delete. The
migration introduced two, and R3b's inherited 37-file budget was wide enough to
hide both.

- The collaborative socket provider moves to `components/socket-provider`. It
  has consumers on both sides of the route tree — the workspace editor mounts
  it, and the file view's collaboration hook reads it from inside a unit — so
  `app/workspace/providers/**` was never its home. Same fix, and same reasoning,
  as the earlier `ActionRow` / `AnchoredContextMenu` moves.

- `DELETED_WORKFLOW_LABEL` moves to `lib/workflows/labels.ts`. It is a workflow
  concept, not a log one: four of its five consumers are workflow-editor
  surfaces, and sourcing it from the log-view barrel made `lib/workflows/
  subblocks/display.ts` — reachable from server routes — drag a 762-line
  'use client' module behind it.

- The log view's frozen-canvas modal keeps its `Preview` import, annotated. That
  component reads useParams<{ workspaceId }>() and pulls in the editor's
  subblock and edge components; it IS editor UI, and hoisting it would drag half
  the editor into components/. It is safe only because `log`'s share seed is
  `never`.

Adds R3c to check:resources, held at 0: the same edge R3b counts, narrowed to
canonical units, where there is no budget. Verified it fails by removing the
annotation and watching the audit go red.
…it finished"

headObject now answers local storage from `stat` instead of reporting every
object as missing. That is the honest answer and the byte-range paths need it,
but it also brought ten existing callers to life on self-hosted and dev
deployments — including three that read "the object is there" as "the copy
finished" and skip the work: the workspace fork copier, the KB document copier,
and the table snapshot cache.

Those guards were written against cloud backends, where an interrupted PUT
leaves no object at all. The local path was a bare `writeFile`, which truncates
before it writes, so a crash mid-write would have left a prefix under a key
those callers now treat as complete — a fork retry marking a torn file copied.

Writes now land on a sibling `.partial` path and are renamed onto the target;
`rename` within a directory is atomic on POSIX and replaces in one step, so the
key names either the previous bytes or the whole new ones. A failed write or
rename removes the temp file and rethrows. Local multipart accumulates and
commits through this same path, so snapshots are covered too.

Reviewed the other seven callers: all strictly improve. materialize-file now
charges quota against the real on-disk size rather than trusting the row, the
CSV import progress bar is no longer permanently indeterminate on local, and
the TikTok size probe skips its counting pass. Their comments claimed local
returns null; refreshed.

Tests assert the call sequence rather than an observed torn read — a timing
test passes against a plain `writeFile` too, since even a multi-megabyte write
resolves inside one macrotask, so it would be a test that cannot fail.
Confirmed all four go red when the temp-and-rename is reverted.
Each of these is a side-effect of a deliberate change reaching further than the
change needed. The features stay; what they touched by accident is put back.

- Config files download again. Resolving unknown extensions through the
  canonical MIME table is what makes byte-range serving work, but it also
  promoted .env/.log/.conf/.ini/.cfg from "unknown bytes" to text/plain, which
  is on the inline allowlist — a shared .env started rendering in the tab
  instead of downloading. Those five join FORCE_ATTACHMENT_EXTENSIONS, so the
  MIME resolution stays honest and the disposition stays what it was. Verified
  these are the only five of the 47 newly-typed extensions that flipped.

- The aggregate per-share ceiling is charged once per read, not once per seek.
  It is shared by every visitor to a link, so counting each range request let
  one person scrubbing a shared video 429 everyone else. The audit row beside it
  already deduped seeks; both now use one `isReadStart` predicate rather than
  two copies of the same string test that can drift apart.

- MediaPreview clears a decode failure when the bytes change. It is keyed on
  `file.id`, which is stable across content edits, so one transient error wedged
  the player until the viewer opened a different file. Adjusted during render,
  the same pattern the markdown image node view uses.

- The chat-trigger attachment guard accepts `dataUrl`, the preferred inline
  field, not just the legacy `data`. `processChatFiles` reads `dataUrl || data`,
  so testing only `data` skipped the upload for every current client and dropped
  its attachments silently — the exact failure the guard was added to prevent.

- The navbar sticks below the macOS traffic-light lane instead of the viewport.
  Every shell mounting it wears `.desktop-title-bar-page`, which reserves the
  lane with padding, but sticky positions against the scroll port — so the bar
  slid under the traffic lights on scroll. The variable is 0px off-desktop.

- The wordmark opens in a new tab on the deployed chat and the public file page.
  Reusing the landing navbar turned those two from `sim.ai` in a new tab into
  same-origin `/` in the same tab, so clicking the logo mid-conversation
  navigated away and destroyed it. `/` is the better href — self-hosters get
  their own landing — so only the target moves back, behind one `brandInNewTab`
  prop for surfaces hosting something the viewer would lose.
… have

An audit of all four units against each other found one real divergence and a
pile of small ones, all in table-view. file-view, log-view and knowledge-view
are already uniform: `<unit>.tsx` + `index.ts` + `components/<child>/` + `utils/`
(+ `hooks/`), absolute imports, tests colocated with what they test. table-view
was flat — fifteen entries at the unit root, `cells/` and `headers/` as sibling
groups rather than children of `components/`, three test files at the root, and
relative imports throughout, which the repo bans everywhere else.

Nothing about that was load-bearing. This moves the files and rewrites the
specifiers; the only non-import lines in the diff are five type-only import
members biome reflowed. Behaviour is unchanged by construction, and page and
panel keep mounting the same components they did.

  cells/, headers/            -> components/
  data-row, select-pill, ...  -> components/<name>/<name>.tsx + index.ts
  utils.ts, values.ts,        -> utils/selection.ts, utils/values.ts,
  constants.ts                   utils/constants.ts
  *.test.ts                   -> beside their subject

What this does NOT fix, and deliberately: table-view still has no `TableView`.
It exports 72 symbols and no view, because a table has no read-only surface yet
— everything that draws a grid today also writes one, and the shell that does
both reads useRouter and the permission context. Splitting it is a real
refactor, not a move, and it is the one thing standing between `table` and the
same axes the other three are mounted against. The barrel and the rule now say
so plainly instead of implying uniformity that was not there.

Also corrects three docs that claimed `knowledge` and `log` had no canonical
view. They both do; only `table` does not.
…text

Step 4, and the step where a mistake would actually change who can do what.
45 permission reads converted:

- 38 `userPermissions.canEdit` -> `grants.write`. Mechanically, with no
  exceptions: `grants.run` is `canEdit || canRead`, so a single slip on one of
  the run/stop controls would have handed a read-only member the ability to
  trigger workflow runs from the panel. Verified zero `grants.run` references
  survive in the tables tree outside one TSDoc that explains the hazard.
- 4 `userPermissions.canAdmin` -> `grants.manage`, all of them lock settings.
- 2 `userPermissions.isLoading` -> `!grants.settled`. This is the one that
  needed the axis extension: the lock notice is a one-shot latched on
  `announcedLockTableIdRef`, so firing it before capabilities resolve
  permanently yields a toast whose "Lock settings" action is missing.

`page.tsx` is a Server Component and cannot read a React context, so the table
route gains a ~40-line client shell (`table-route.tsx`) that resolves the axes
and mounts the view — the same shape as `fullscreen-file-view.tsx`. Reading
`useParams()` there is legitimate where it was not inside the table: a route
shell exists exactly once per page by definition, which is precisely the
property the table lost when the panel started mounting it too.

The panel already computed `grants` for its file and log branches; the table
branch now passes the same value.
Step 5. Two `router.push` calls, both to the tables list, both hand-building
`/workspace/${workspaceId}/tables`.

The table now builds its own `workspaceSource` and asks it for the target —
`source.hrefFor({ to: 'list' })`, the member added for exactly this — and hands
the result to an `onNavigate` prop. The route table stays in one file, and a
host that owns no router simply omits the prop, which makes navigation inert by
construction rather than by a check at each call site.

The source is built inside the table rather than taken as a prop because
`page.tsx` is a Server Component and a source carries closures, which cannot
cross the RSC boundary. That is why the axes reach the table through its client
shell in the first place.

Both hosts supply `onNavigate`: the route shell as `router.push`, the panel
reusing the `navigate` callback its file and log branches already pass.
Step 6. The table borrowed the logs page's `LogDetails` shell to show a run.
That shell reads `useParams`, `useRouter`, `useQueryState` and the permission
context — four route couplings a table has no use for — and it hard-codes
`host='page'`.

The `useQueryState` is the interesting one: it deep-links the log's active tab
unconditionally, so opening a run from the *tables* page wrote `?tab=trace` onto
the tables URL, and onto the host page's URL from the chat panel. A second
instance of the same bug step 3 fixed, in a component nobody thought of as
URL-owning.

`ExecutionSlideout` reproduces only the chrome the table actually uses — frame,
resize handle, close button — and mounts the canonical `LogView` itself. It omits
`tab`/`onTabChange`, so the view keeps the tab local, which is correct for a
panel that was never addressable. The logs page's prev/next and retry controls
are dropped deliberately: they navigate a list this surface does not have, and
the table only ever passed three of that shell's ten props.

`showExecutionInternals` becomes a required prop on the table, forwarded from
both hosts. Not read from `usePermissionConfig` inside the table — although R6
would allow it — because passing R6 by an accident of regex coverage is not the
same as being right, and a permission-group restriction should fail to compile
when a host forgets it rather than default to revealing payloads.

Cross-route edges out of the tables tree: 3 left, and only one of them is real.
Step 7. The table implementation moves out of the route tree and into
`components/resources/table-view/`, joining the layout the unit already had:
`table-view.tsx`, `index.ts`, `components/<child>/`, `hooks/`, `utils/`,
`types.ts`. What stays behind is four route files — `page.tsx`, `loading.tsx`,
`error.tsx`, and the ~50-line client shell that resolves the axes.

`Table` becomes `TableView` and takes `source: ResourceSource<'table'>` instead
of `workspaceId` + `tableId`. Both are banned prop names, and registering the
view in `CANONICAL_UNITS` turns R4a/R4b on to say so — the gate found them
immediately. The subtree below still takes plain ids, so the source is narrowed
once at the top through `tableWorkspaceId`/`tableResourceId`, which is the
helper deleted earlier for having no caller, restored now that it has one.

The four preceding steps are what made this a move rather than a rewrite: with
the router, the params and the permission context already severed, R6 was
already 0 before a single file changed directory. 61 files, ~160 lines of
non-import churn.

One edge stays and is annotated: the workflow-column sidebar renders the
editor's read-only canvas, which reads `useParams` and pulls in the editor's
node and edge components. Safe for the same reason the log view's frozen canvas
is — `ResourceSeedMap['table']` is `never`, so no anonymous surface can mount
this unit — and `CROSS_TREE_ALLOWLIST` now names two files pointing at one
component, which is the signal that `Preview` itself wants to move to
`components/`. That is a separate change.

Every rule back at baseline with the view registered: R2 0, R3c 0, R4a 0, R4b 0,
R6 0.
The rules, CLAUDE.md and the migration doc all still described `table` as the
holdout and the table unit as "view layer only". Corrects the status table, and
two claims that are now wrong in a way that would mislead:

- "A view is read-only; the shell writes." That was a staging point during the
  migration, never a rule — `FileView` edited from the start, and `TableView`
  does now. What a view must not hold is route context, not mutations.
- "`table-grid.tsx` never moved, and did not need to." True of the first pass.
  It moved in the second, intact, and the ordering is the lesson worth keeping:
  the context reads were severed *in place* first, so R6 was already 0 before a
  single file changed directory. The 61-file move then carried no semantic risk.
  Reversed, four behavioural edits would have been buried inside a rename diff.

Also drops the "mothership still mounts the whole editing page" note from the
open list — it mounts `TableView` against a source now, like files and logs.
…nits use

Two deviations this migration introduced, found by diffing all four views'
prop surfaces against each other rather than reading them one at a time.

- The axes did not lead the interface. `source` had replaced `workspaceId` +
  `tableId` in the slot those occupied at the bottom, so the view declared
  `host, grants, … source`. Every other unit reads `source, grants, host` first.

- Two loose optional feature booleans. The rule says collapse into one object
  and never add a loose prop, and the optional default was load-bearing in the
  wrong direction: nobody decided the embedded table has no lock settings —
  `tableLocksEnabled?: boolean` decided it by omission. `features:
  TableViewFeatures` is required, so the panel now states that answer in a
  comment instead of inheriting it from a default. Both fields disappear
  together when the flags GA, which is the other reason they travel as one
  object rather than spread.

Behaviour is unchanged: the panel passes `locks: false`, which is what the
absent prop already resolved to.
Same ordering that made the table move safe: the behavioural edits land first,
as a diff against files that have not moved.

- `useParams` -> `knowledgeWorkspaceId(source)`. The shell took `workspaceId?`
  optionally and fell back to route params, which the page relied on — so the
  fallback was load-bearing, and it is what stopped this surface existing more
  than once per page.
- `useRouter` -> `onNavigate`. Three pushes: the document detail route, the
  post-delete redirect, and the breadcrumb root. The latter two already went
  through `hrefFor({ to: 'list' })`; the first now goes through a new
  `knowledgeDocumentHref`, which lives in a per-kind module because a document
  is not a `ResourceKind` — it has no view, no grants and no share surface, so
  it cannot be spelled through `hrefFor({ to: 'resource' })`.
- `useUserPermissionsContext` -> `grants`. 13 reads, including two of the form
  `canEdit || isLoading` that gate whether a breadcrumb dropdown renders at all.
  Those become `grants.write || !grants.settled` — the exact pattern `settled`
  was added for, and without it the caret would have popped in after load
  instead of rendering disabled.

Three supporting relocations, all pure moves:
- `knowledge/components/icons` deleted — byte-identical to
  `@/components/icons/document-icons`, confirmed with `diff`.
- `use-knowledge-upload` -> `@/hooks/kb/`.
- `useContextMenu` -> `components/anchored-context-menu/`, beside the component
  every one of its 13 consumers pairs it with.

Also makes table-view's internal imports absolute. They arrived relative with
the move and I did not rewrite them; log-view and knowledge-view have zero
relative imports, so table-view was the outlier against the rule this PR
documents. (file-view has 83 and is left alone — pre-existing, and not worth
83 files of churn here.)
The panel now mounts `KnowledgeView` and imports nothing from the workspace
route tree. That was the last of the four kinds.

The shape of this one differs from tables, and the difference is the useful
part: the route shell owned the *whole* surface — header, breadcrumbs, ten
modals, bulk operations, the context menu — while the extracted `KnowledgeView`
was only the document list. So the shell became the view, and the previous view
became its `DocumentList` child. Promoting the read-only half and leaving the
mutations behind would have left the panel importing the route tree forever.

Dropped `id` from the props: `source` already carries it (`knowledgeResourceId`),
and two spellings of the same identity is exactly what the axes replace.
`knowledgeBaseName` stays, documented as a title hint rather than identity — it
is what keeps both hosts from flashing a placeholder header before load.

Four boundary violations surfaced by `check:resources` once the files were
inside a unit, each fixed at the cause rather than annotated:

- `AddDocumentsModal` read `useParams()` for the workspace id. Inside the panel
  there is no `[workspaceId]` segment, so this was already returning undefined
  there — the modal is reachable from `/home`. Now a required prop.
- `use-connector-config-fields` was route-tree-resident but is unit-internal;
  moved to `@/hooks/kb/`.
- `BreadcrumbItem`/`ResourceAction` came from the route barrel that merely
  re-exports `@/components/resource`. Retargeted to the source.
- The knowledge list page and the document route imported past the unit barrel.
  They render the same tag editor and selection bar against the same data, so
  the barrel exports those three children — and the rule now says when that is
  allowed, because "saves an import hop" must not qualify.

`ConnectOAuthModal` and `WorkspaceHostProvider` looked like they pinned the
connector modals to the route tree. Neither is route-specific: both moved to
`components/` (with `WorkspaceAccessDenied`, which the provider renders), 22
importers repointed. Deleting the blocker beat splitting the surface around it.

nuqs stays outside the unit — R6 has no escape hatch — so
`use-knowledge-list-state` moved to `@/hooks/kb/` and its parsers to
`@/lib/knowledge/detail-search-params`, mirroring the table.

Suite: 20917 passed, unchanged from before the move. R6 and R3c both 0.
Six conflicts, all one cause: staging's unbolding sweep (#6400) landed on code
this branch had moved out of the route tree.

Resolved by keeping this branch's structure and replaying every one of staging's
21 `font-medium` removals at the element's new home — 16 applied, the rest
already absorbed by the auto-merge. Verified none of the strings staging
unbolded is still bold anywhere in the tree.

One deliberate deviation: staging rewrote the file-preview fallback as
`text-[14px]`, which the styling rule forbids (font-size only, inherits the
wrong line-height). Took staging's intent — dropping `font-medium` — and kept
the named `text-sm`, which is the same 14px.

`base.tsx` and `file-viewer.tsx` came back as delete/modify conflicts because
they no longer exist here; their changes were ported into `document-list.tsx`
and the file-view unit rather than dropped.

Suite: 20951 passed. R6/R3c 0, 23 audits green.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (622 files, 500 file limit).

Cursor caught a real regression, and it was mine. The execution slideout
forwarded the table's `host` into `LogView` while deliberately passing no
`tab`/`onTabChange` — so on the tables page `hostOwnsUrl` was true, `activeTab`
was pinned to `tab ?? 'overview'`, and `setActiveTab` called an undefined
callback. The Trace tab was unreachable when opening a run from the tables page.
The panel host was unaffected, which is why the suite stayed green.

The slideout's own TSDoc already claimed the tab stayed local. The doc described
the intent; forwarding `host` contradicted it.

Fixed at both levels:

- The slideout mounts the log as `host='panel'` outright and no longer takes a
  `host` prop. A run slid in beside a table is embedded no matter where that
  table renders — it has no address on either surface, so the tab is local state
  by definition, not by inheritance from whatever the table happens to be.
- `LogView` treats the tab as URL-owned only when the host actually wired the
  controlled pair (`hostOwnsUrl(host) && onTabChange !== undefined`). A host that
  owns its URL but supplies no setter is embedding this view somewhere
  unaddressable; degrading to local state keeps the tabs working. Dead tabs are
  strictly worse than a tab that does not persist.

The guard rather than a discriminated union on `host`, because the table passes
`host` as a variable — a type-level union would not have fired at that mount.

Suite: 20951 passed.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/files/public/[token]/content/route.ts
Comment thread apps/sim/app/api/files/public/[token]/content/route.ts Outdated
Both from Cursor, both real, both in the byte-range work rather than the view
migration.

**The per-share ceiling was bypassable with any non-start Range.** The limit was
gated on `isReadStart(range)` so a player scrubbing a video would not 429 every
other visitor to the link. But that predicate was read before knowing whether
the file was media, and only media honours `Range` — every other response
ignores the header and returns the whole body. So `Range: bytes=1-` on a shared
PDF took the full download and never touched the share budget.

The exemption now applies only where the response actually is byte-served.
Media still gets its free seeks; everything else is charged unconditionally.

**Shares classified media differently than the workspace does.** This route
keyed off the stored `file.contentType`; `serve/[...path]` keys off
`getContentType(displayName)`. Uploads routinely land as
`application/octet-stream`, so a shared `.mp4` fell out of the byte-range branch,
lost `Accept-Ranges`, and could not be scrubbed — while the identical file stayed
seekable inside the workspace. Now classified by filename as well as stored type,
which is a superset of what the workspace path accepts, so nothing that is
seekable there is unseekable here.

`rangeHeader` is read once at the top now instead of twice, since both the limit
decision and the audit row need it.

Suite: 20951 passed. files API: 267 passed.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/files/public/[token]/content/route.ts
Follow-on to the previous commit, and a hole that commit opened.

Widening `isMedia` to admit filename-detected media let an `application/octet-stream`
`.mp4` into the byte-range branch — but the response still declared
`contentType: file.contentType`. These responses are `nosniff`, so the browser
saw `application/octet-stream` and refused to render, and
`getSecureFileHeaders` derives disposition from that same value, so it also came
back as `attachment`. The file became seekable and still would not play. The fix
moved the failure instead of removing it.

The declared type is now resolved alongside the admission decision: the stored
type wins when it is already audio/video (more specific — `video/webm` beats a
filename guess), otherwise the filename-derived type that admitted the file.
Header and disposition both follow it.

The buffered branch sixty lines below already resolved its type this way, with a
comment naming this exact hazard. I widened the predicate without reading it.

Suite: 20951 passed. files API: 267 passed.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d512856. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant